using System;
using System.Collections.Generic;
using System.Text;

using csammisrun.utility;
using csammisrun.MsnLib.Utility;

namespace csammisrun.MsnLib
{
  public class Session
  {
    /// <summary>
    /// Identifies the version of MsnLib that is in use
    /// </summary>
    public const string MSNLIB_VERSION = "0.1";
    /// <summary>
    /// Identifies the highest protocol version supported by MsnLib
    /// </summary>
    public const int PROTOCOL_VERSION = 9;
    public const int CLIENT_VERSION = 0x40;

    private bool _usehttp = false;
    private bool _isloggedin = false;
    private bool _isclientlogging = false;
    private string _username = "";
    private string _password = "";
    private string _friendlyname = "";
    private string _iconpath = "";
    private ClientCapabilities _capabilities = ClientCapabilities.Basic;
    private Notification _notificationboard = null;
    private Nexus _nexus = null;
    private User _self = null;
    private PassportInfo _passportinfo = new PassportInfo();
    private UserList _userlist = new UserList();
    private UserList _blocklist = new UserList();
    private UserList _allowlist = new UserList();
    private UserList _reverselist = new UserList();
    private List<Switchboard> _switches = new List<Switchboard>();
    private List<ChatRoom> _chatrooms = new List<ChatRoom>();
    private UserStatus _status = UserStatus.Available;

    #region Event handlers
    #region Nexus error events
    void _nexus_AuthenticationFailed(Nexus nexus, string message)
    {
      this.OnLoginFailed(LoginFailure.BadPassword);
    }

    void _nexus_ConnectionFailed(Nexus nexus, string message)
    {
      this.OnLoginFailed(LoginFailure.CouldntConnectToNexus);
    }
    #endregion

    #region Login events
    /// <summary>
    /// Occurs when a step in the login process has completed
    /// </summary>
    public event LoginProgressUpdateHandler LoginProgressUpdate;
    /// <summary>
    /// Raises the <see cref="LoginProgressUpdated"/> event
    /// </summary>
    protected internal void OnLoginProgressUpdated(LoginStep step)
    {
      if (LoginProgressUpdate != null)
      {
        LoginProgressUpdate(this, step);
      }
    }

    /// <summary>
    /// Occurs when login has completed and the session is ready for use
    /// </summary>
    public event LoginCompleteHandler LoginComplete;
    /// <summary>
    /// Raises the <see cref="LoginComplete"/> event
    /// </summary>
    protected internal void OnLoginComplete()
    {
      if (!String.IsNullOrEmpty(this.IconPath))
      {
        _self.SetBuddyIcon(this.IconPath);
      }
      NotificationBoard.Processor.UseThreadPoolDispatch = true;
      NotificationBoard.StartKeepaliveTimer();
      Userlist.IsSyncing = false;

      _isloggedin = true;

      this.Status = this.Status; // This allows for the client to set its own status prior to connection
      if (LoginComplete != null)
      {
        LoginComplete(this);
      }
    }

    /// <summary>
    /// Occurs when the login process fails
    /// </summary>
    public event LoginFailedHandler LoginFailed;
    /// <summary>
    /// Raises the <see cref="LoginFailed"/> event
    /// </summary>
    protected internal void OnLoginFailed(LoginFailure reason)
    {
      if (LoginFailed != null)
      {
        LoginFailed(this, reason);
      }
    }
    #endregion

    #region Userlist events
    /// <summary>
    /// Occurs when a <see cref="Group"/> has been received by the server during userlist synchronization
    /// </summary>
    public event GroupItemReceivedHandler GroupItemReceived;
    /// <summary>
    /// Raises the <see cref="GroupItemReceived"/> event
    /// </summary>
    protected internal void OnGroupItemReceived(Group group)
    {
      this.Userlist.Groups.Add(group);
      if (GroupItemReceived != null)
      {
        GroupItemReceived(this, group);
      }
    }

    /// <summary>
    /// Occurs when a <see cref="User"/> has been received by the server during userlist synchronization
    /// </summary>
    public event UserItemReceivedHandler UserItemReceived;
    /// <summary>
    /// Raises the <see cref="UserItemReceived"/> event
    /// </summary>
    protected internal void OnUserItemReceived(User user)
    {
      this.Userlist.Users.Add(user);
      if (UserItemReceived != null)
      {
        UserItemReceived(this, user);
      }
    }

    /// <summary>
    /// Occurs when a user's status has been updated by the server
    /// </summary>
    public event UserStatusUpdateReceivedHandler UserStatusUpdateReceived;
    /// <summary>
    /// Raises the <see cref="UserStatusUpdateReceived"/> event
    /// </summary>
    protected internal void OnUserStatusUpdateReceived(User user)
    {
      if (UserStatusUpdateReceived != null)
      {
        UserStatusUpdateReceived(this, user);
      }
    }

    /// <summary>
    /// Occurs when a remote user has added the local user to their contact list
    /// </summary>
    /// <remarks>This event must be responded to using <see cref="AllowUserListAddition"/> or <see cref="DenyUserListAddition"/></remarks>
    public event UserListAdditionRequestHandler UserListAdditionRequest;
    /// <summary>
    /// Raises the <see cref="UserListAdditionRequest"/> event
    /// </summary>
    /// <param name="user"></param>
    protected internal void OnUserListAdditionRequest(User user)
    {
      if (UserListAdditionRequest != null)
      {
        UserListAdditionRequest(this, user);
      }
    }
    #endregion

    #region Notification object events
    /// <summary>
    /// Raised when the notification board has been disconnected
    /// </summary>
    void _notificationboard_Disconnected(Notification notification)
    {
      if (IsLoggedIn)
      {
      }
      _isloggedin = false;
    }

    /// <summary>
    /// Raised when the notification board's connect process has failed
    /// </summary>
    void _notificationboard_ConnectionFailed(Notification notification, string error)
    {
      if (!IsLoggedIn)
      {
        OnLoginFailed(LoginFailure.CouldntConnectToService);
      }
      _isloggedin = false;
    }

    /// <summary>
    /// Occurs when the Hotmail server sends notification of new email received
    /// </summary>
    public event InitialEmailNotificationReceivedHandler InitialEmailNotificationReceived;
    /// <summary>
    /// Raises the <see cref="EmailNotificationReceived"/> event
    /// </summary>
    protected internal void OnInitialEmailNotificationReceived(int inboxunread, int foldersunread)
    {
      if (InitialEmailNotificationReceived != null)
      {
        InitialEmailNotificationReceived(this, inboxunread, foldersunread);
      }
    }

    /// <summary>
    /// Occurs when a new email message is received by the client's account
    /// </summary>
    public event EmailNotificationReceivedHandler EmailNotificationReceived;
    /// <summary>
    /// Raises the <see cref="EmailNotificationReceived"/> event
    /// </summary>
    protected internal void OnEmailNotificationReceived(string from, string subject)
    {
      if (EmailNotificationReceived != null)
      {
        EmailNotificationReceived(this, from, subject);
      }
    }
    #endregion

    #region Switchboard events
    /// <summary>
    /// Occurs when a user joins a <see cref="ChatRoom"/>
    /// </summary>
    public event ChatRoomUserJoinedHandler ChatRoomUserJoined;
    /// <summary>
    /// Raises the <see cref="ChatRoomUserJoined"/> event
    /// </summary>
    protected internal void OnChatRoomUserJoined(ChatRoom chatroom, User user)
    {
      if (ChatRoomUserJoined != null)
      {
        ChatRoomUserJoined(this, chatroom, user);
      }
    }

    /// <summary>
    /// Occurs when a user leaves a <see cref="ChatRoom"/>
    /// </summary>
    public event ChatRoomUserLeftHandler ChatRoomUserLeft;
    /// <summary>
    /// Raises the <see cref="ChatRoomUserLeft"/> event
    /// </summary>
    protected internal void OnChatRoomUserLeft(ChatRoom chatroom, User user)
    {
      if (ChatRoomUserLeft != null)
      {
        ChatRoomUserLeft(this, chatroom, user);
      }
    }

    /// <summary>
    /// Occurs when a remote user is typing
    /// </summary>
    public event TypingNotificationReceivedHandler TypingNotificationReceived;
    /// <summary>
    /// Raises the <see cref="TypingNotificationReceived"/> event
    /// </summary>
    protected internal void OnTypingNotificationReceived(User user)
    {
      if (TypingNotificationReceived != null)
      {
        TypingNotificationReceived(this, user);
      }
    }

    /// <summary>
    /// Occurs when a member of a chatroom is typing
    /// </summary>
    public event ChatRoomTypingNotificationReceivedHandler ChatRoomTypingNotificationReceived;
    /// <summary>
    /// Raises the <see cref="ChatRoomTypingNotificationReceived"/> event
    /// </summary>
    protected internal void OnChatRoomTypingNotificationReceived(ChatRoom chatroom, User user)
    {
      if (ChatRoomTypingNotificationReceived != null)
      {
        ChatRoomTypingNotificationReceived(this, chatroom, user);
      }
    }

    /// <summary>
    /// Occurs when an instant message is received
    /// </summary>
    public event MessageReceivedHandler MessageReceived;
    /// <summary>
    /// Raises the <see cref="MessageReceived"/> event
    /// </summary>
    protected internal void OnMessageReceived(User user, string htmlmessage)
    {
      if (MessageReceived != null)
      {
        MessageReceived(this, user, htmlmessage);
      }
    }

    /// <summary>
    /// Occurs when a message is received in a chatroom
    /// </summary>
    public event ChatRoomMessageReceivedHandler ChatRoomMessageReceived;
    /// <summary>
    /// Raises the <see cref="ChatRoomMessageReceived"/> event
    /// </summary>
    protected internal void OnChatRoomMessageReceived(ChatRoom chatroom, User user, string htmlmessage)
    {
      if (ChatRoomMessageReceived != null)
      {
        ChatRoomMessageReceived(this, chatroom, user, htmlmessage);
      }
    }

    /// <summary>
    /// Occurs when a user has sent a Nudge message
    /// </summary>
    /// <remarks>If any client using MsnLib actually uses this event to physically move a conversation window, it'll make baby Jesus cry.</remarks>
    public event NudgeReceivedHandler NudgeReceived;
    /// <summary>
    /// Raises the <see cref="NudgeReceived"/> event
    /// </summary>
    protected internal void OnNudgeReceived(User user)
    {
      if (NudgeReceived != null)
      {
        NudgeReceived(this, user);
      }
    }

    /// <summary>
    /// Occurs when a user has invited the local user to receive a file
    /// </summary>
    public event FileTransferInvitationReceivedHandler FileTransferInvitationReceived;
    /// <summary>
    /// Raises the <see cref="FileTransferInvitationReceived"/> event
    /// </summary>
    protected internal void OnFileTransferInvitationReceived(User inviter, string filename, int filesize, string cookie)
    {
      if (FileTransferInvitationReceived != null)
      {
        FileTransferInvitationReceived(this, inviter, filename, filesize, cookie, null);
      }
    }

    protected internal void OnFileTransferInvitationReceived(User inviter, string filename, int filesize, string cookie, System.IO.MemoryStream preview)
    {
      if (FileTransferInvitationReceived != null)
      {
        FileTransferInvitationReceived(this, inviter, filename, filesize, cookie, preview);
      }
    }

    /// <summary>
    /// Occurs when a file transfer that was previously sent by the local user was cancelled
    /// </summary>
    public event FileTransferCancelledHandler FileTransferCancelled;
    /// <summary>
    /// Raises the <see cref="FileTransferCancelled"/> event
    /// </summary>
    protected internal void OnFileTransferCancelled(string cookie, string reason)
    {
      if (FileTransferCancelled != null)
      {
        FileTransferCancelled(this, cookie, reason);
      }
    }
    #endregion

    #region Internal event handlers
    internal void OnError(MsnErrorCode code)
    {
    }
    #endregion
    #endregion

    /// <summary>
    /// Creates a new MSN session
    /// </summary>
    /// <param name="username">The Passport account of the user to log in</param>
    /// <param name="password">The password for the account</param>
    public Session(string username, string password)
    {
      if (String.IsNullOrEmpty(username))
      {
        throw new ArgumentException("Username cannot be empty");
      }

      if (String.IsNullOrEmpty(password))
      {
        throw new ArgumentException("Password cannot be empty");
      }

      this.Username = username;  // This will throw a FormatException if the username is malformed
      this.Password = password;

      _socketfactory = new ProxiedSocketFactoryDelegate(Session.DirectSocketConnectionFactory);

      _notificationboard = new Notification(this);
      _notificationboard.ConnectionFailed += new NotificationConnectionFailedHandler(_notificationboard_ConnectionFailed);
      _notificationboard.Disconnected += new NotificationDisconnectedHandler(_notificationboard_Disconnected);

      _nexus = new Nexus(this);
      _nexus.ConnectionFailed += new NexusConnectionFailedHandler(_nexus_ConnectionFailed);
      _nexus.AuthenticationFailed += new NexusAuthenticationFailedHandler(_nexus_AuthenticationFailed);
    }

    #region Methods
    /// <summary>
    /// Initialize the logging system
    /// </summary>
    public void InitializeLogger()
    {
      string logfile = DateTime.Now.ToString().Replace("/", "").Replace(":", "") + ".MsnLib.log";
      Logging.Init(logfile);
      Logging.EchoToConsole = true;
    }

    /// <summary>
    /// Logs into the Messenger service
    /// </summary>
    public void Login(string hostname, int port)
    {
      // Don't log in if it's already being used
      if (this.NotificationBoard.InUse)
      {
        return;
      }

      // Connect the notification board, we can signal success when
      // the notification system gives us the contact list and a SYN
      this.NotificationBoard.Connect(hostname, port);
    }

    /// <summary>
    /// Logs out of the Messenger service
    /// </summary>
    public void Logout()
    {
      if (!IsLoggedIn)
      {
        return;
      }

      _isloggedin = false;
      foreach (Switchboard sb in Switches)
      {
        sb.Disconnect();
      }

      this.NotificationBoard.Disconnect();
    }

    /// <summary>
    /// Sends an HTML message to a user
    /// </summary>
    public void SendMessage(string user, string text)
    {
      if (!IsLoggedIn)
      {
        return;
      }

      Switchboard sb = FindSwitchboardBySubtype(user, SwitchboardSubType.InstantMessage, true);
      sb.SendMessage(Message.HTMLToTextMessage(text));
    }

    /// <summary>
    /// Sends a notification that the client is typing a message to the specified user
    /// </summary>
    /// <param name="user"></param>
    public void SendTypingNotification(string user)
    {
      if (!IsLoggedIn)
      {
        return;
      }

      Switchboard sb = FindSwitchboardBySubtype(user, SwitchboardSubType.InstantMessage, false);
      if(sb != null)
      {
        sb.SendTypingNotification();
      }
    }

    /// <summary>
    /// Sends a notification that the client is typing a message to the specified chatroom
    /// </summary>
    public void SendTypingNotificationToChatroom(ChatRoom chatroom)
    {
      if (!IsLoggedIn)
      {
        return;
      }

      chatroom.Switchboard.SendTypingNotification();
    }

    /// <summary>
    /// Sends an HTML message to a chatroom
    /// </summary>
    public void SendMessageToChatroom(ChatRoom chatroom, string text)
    {
      if (!IsLoggedIn)
      {
        return;
      }

      chatroom.Switchboard.SendMessage(Message.HTMLToTextMessage(text));
    }

    /// <summary>
    /// Determines if the argument is a valid Passport username
    /// </summary>
    public bool ValidateMsnUsername(string username)
    {
      try
      {
        return true;  // NO WHAT THE HELL NO NO NO
        Uri test = new Uri("mailto:" + username);
        return test.Scheme != Uri.UriSchemeMailto;
      }
      catch (UriFormatException)
      {
        return false;
      }
    }

    /// <summary>
    /// Add a user a userlist
    /// </summary>
    public void AddUserToList(string passport, Group group, UserListType list)
    {
      AddUserToList(passport, passport, group, list);
    }

    /// <summary>
    /// Accept a request to add a remote user to the contact list
    /// </summary>
    public void AllowUserListAddition(User user)
    {
      AddUserToList(user, UserListType.Forward);
    }

    /// <summary>
    /// Deny a request to add a remote user to the contact list
    /// </summary>
    public void DenyUserListAddition(User user)
    {
      AddUserToList(user, UserListType.Block);
    }

    /// <summary>
    /// Add a <see cref="User"/> to a userlist
    /// </summary>
    public void AddUserToList(User user, UserListType list)
    {
      if (user.Groups.Count == 0)
      {
        AddUserToList(user.Passport, user.DisplayName, null, list);
      }
      else
      {
        foreach (Group g in user.Groups)
        {
          AddUserToList(user.Passport, user.DisplayName, g, list);
        }
      }
    }

    /// <summary>
    /// Add a user to a userlist
    /// </summary>
    /// <remarks>
    /// <para>The other two overloads of <see cref="AddUserToList"/> call this method.</para>
    /// <para>If <paramref name="group"/> is <c>null</c>, the user is automatically added to the "groupless" group.</para>
    /// <para>If <paramref name="list"/> is not <see cref="UserListType.ForwardList"/>, the <see cref="group"/> is not taken into account.  Only items on the Forward list can have groups.</para>
    /// </remarks>
    public void AddUserToList(string passport, string alias, Group group, UserListType list)
    {
      int groupid = -1;
      if (group != null)
      {
        groupid = group.ID;
      }

      this.NotificationBoard.AddContact(passport, alias, groupid, list);
    }

    public void CancelFileTransfer(string cookie)
    {
      P2PInfo info = P2PMessage.GetInfoBySID(cookie);
      if (info != null)
      {
        Switchboard sb = FindSwitchboardBySubtype(info.Destination, SwitchboardSubType.InstantMessage, false);
        if (sb != null)
        {
          sb.SendFileTransferCancel(cookie);
        }
      }
    }

    /// <summary>
    /// Implements an asynchronous socket connection factory that operates with no network proxy
    /// </summary>
    /// <param name="host">The host to which to connect</param>
    /// <param name="port">The port on the host to which to connect</param>
    /// <remarks>This is a public wrapper to the internal method <see cref="Connection.CreateDirectConnectSocket"/></remarks>
    public static void DirectSocketConnectionFactory(string host, int port, Delegate callback)
    {
      ServerConnection.CreateDirectConnectSocket(host, port, callback);
    }
    #endregion

    #region Properties
    /// <summary>
    /// Gets or sets the username for the session
    /// </summary>
    public string Username
    {
      get { return _username; }
      set
      {
        if (IsLoggedIn)
        {
          throw new InvalidOperationException("Can't assign username while session is logged in");
        }

        if (ValidateMsnUsername(value) && _username != value)
        {
          _username = value;
        }
        else
        {
          throw new ArgumentException(value + " is not a valid MSN passport");
        }
      }
    }

    /// <summary>
    /// Gets or sets the password for the session
    /// </summary>
    public string Password
    {
      get { return _password; }
      set { _password = value; }
    }

    /// <summary>
    /// Gets or sets the user's "friendly" name
    /// </summary>
    public string FriendlyName
    {
      get { return _self.DisplayName; }
      set
      {
        _self.DisplayName = value;
        // Update on the server
      }
    }

    /// <summary>
    /// Gets a value indicating whether this <see cref="Session"/> is logged in
    /// </summary>
    public bool IsLoggedIn
    {
      get { return _isloggedin; }
      internal set { _isloggedin = value; }
    }

    /// <summary>
    /// Gets or sets a value indicating whether the client using MsnLib is capable of logging chats
    /// </summary>
    public bool IsClientLogging
    {
      get { return _isclientlogging; }
      set { _isclientlogging = value; }
    }

    /// <summary>
    /// Gets or sets a path to a user icon
    /// </summary>
    /// <remarks>If the session is connected, setting this property will begin to upload the new icon</remarks>
    public string IconPath
    {
      get { return _iconpath; }
      set
      {
        if (_iconpath != value)
        {
          _iconpath = value;
          if (IsLoggedIn && _self != null)
          {
            _self.SetBuddyIcon(value);
          }
        }
      }
    }

    /// <summary>
    /// Gets the Forward list of the current session
    /// </summary>
    public UserList Userlist
    {
      get { return _userlist; }
    }

    /// <summary>
    /// Gets a list of users that are on the current user's block list
    /// </summary>
    public UserList BlockedUsers
    {
      get { return _blocklist; }
    }

    /// <summary>
    /// Gets a list of users that have requested to be added to the current user's forward list
    /// </summary>
    public UserList PendingUsers
    {
      get { return _reverselist; }
    }

    /// <summary>
    /// Gets a list of users that have been explicitly allowed access by the current user
    /// </summary>
    public UserList AllowedUsers
    {
      get { return _allowlist; }
    }

    /// <summary>
    /// Gets or sets a value indicating whether MsnLib should use HTTP-baed connections
    /// </summary>
    /// <remarks>By default, MsnLib uses SSL connections.  Set UseHTTP to <c>true</c> if you are behind
    /// a proxy that doesn't allow SSL.</remarks>
    public bool UseHTTP
    {
      get { return _usehttp; }
      set { _usehttp = value; }
    }

    /// <summary>
    /// Gets or sets the capabilities implemented by the client
    /// </summary>
    public ClientCapabilities Capabilities
    {
      get { return _capabilities; }
      set { _capabilities = value; }
    }

    /// <summary>
    /// Gets or sets the current status of the user
    /// </summary>
    public UserStatus Status
    {
      get { return _status; }
      set
      {
        _status = value;
        if (IsLoggedIn)
        {
          NotificationBoard.SetCurrentStatus(_status);
        }
      }
    }


    private ProxiedSocketFactoryDelegate _socketfactory = null;
    /// <summary>
    /// Gets or sets the <see cref="ProxiedSocketFactoryDelegate"/> to use to create new socket connections
    /// </summary>
    /// <remarks>By default, this property is set to <see cref="Session.DirectSocketConnectionFactory"/></remarks>
    public ProxiedSocketFactoryDelegate ProxiedSocketFactory
    {
      get { return _socketfactory; }
      set { _socketfactory = value; }
    }
    #endregion

    #region Internal properties
    /// <summary>
    /// Gets the notification board in use by this session
    /// </summary>
    internal Notification NotificationBoard
    {
      get { return _notificationboard; }
    }

    /// <summary>
    /// Gets the nexus in use by this session
    /// </summary>
    internal Nexus Nexus
    {
      get { return _nexus; }
    }

    /// <summary>
    /// Gets or sets our own User object
    /// </summary>
    internal User Self
    {
      get { return _self; }
      set { _self = value; }
    }

    /// <summary>
    /// Gets a list of switchboards in use by this session
    /// </summary>
    internal List<Switchboard> Switches
    {
      get { return _switches; }
    }

    /// <summary>
    /// Gets a list of chatrooms in use by this session
    /// </summary>
    internal List<ChatRoom> Chatrooms
    {
      get { return _chatrooms; }
    }

    /// <summary>
    /// Gets or sets a string built up from the URL command to act as a Hotmail redirector
    /// </summary>
    internal PassportInfo PassportInfo
    {
      get { return _passportinfo; }
    }

    /// <summary>
    /// Returns the client ID based on the client version and advertised capabilities
    /// </summary>
    /// <returns></returns>
    internal uint GetClientID()
    {
      return CLIENT_VERSION << 24 |
        0x00 << 16 |
        0x00 << 8 |
        (uint)_capabilities;
    }
    #endregion

    #region Internal methods
    /// <summary>
    /// Sets the friendly name of the logged-in user
    /// </summary>
    internal void SetFriendlyName(string name)
    {
      _friendlyname = name;
      // Raise event?
    }

    internal Switchboard FindSwitchboardBySubtype(string passport, SwitchboardSubType subtype, bool create)
    {
      foreach (Switchboard sb in this.Switches)
      {
        if (sb.Type == SwitchboardType.Conversation && sb.SubType == subtype)
        {
          foreach (User u in sb.Participants)
          {
            if (u.Passport == passport)
            {
              return sb;
            }
          }
          // Check the invite list to see if it's pending
          if (sb.UsersToInvite.Contains(passport))
          {
            return sb;
          }
        }
      }

      if (!create)
      {
        return null;
      }

      // Requested switchboard doesn't exist, create one (it'll add itself to the list)
      Switchboard newboard = new Switchboard(this, SwitchboardType.Conversation);
      newboard.SubType = subtype;
      newboard.UsersToInvite.Add(passport);
      newboard.Connect();
      return newboard;
    }

    internal ChatRoom FindChatroomBySwitchboard(Switchboard switchboard)
    {
      if (switchboard.SubType != SwitchboardSubType.ChatRoom)
      {
        return null;
      }

      foreach (ChatRoom cr in _chatrooms)
      {
        if (cr.Switchboard == switchboard)
        {
          return cr;
        }
      }

      return null;
    }
    #endregion
  }
}
